/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkClass, ArkField, Scene, ViewTreeNode, ArkFile } from 'arkanalyzer';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { Rule, MatcherCallback, Defects } from '../../Index';
import { IssueReport } from '../../model/Defects';
import path from 'path';

const gMetaData: BaseMetaData = {
    severity: 1,
    ruleDocPath: 'docs/bad-parameter-passing-check.md',
    description: 'Detects @State+@Prop, @State+@Link, or @State+@Observed+@ObjectLink on the same field.'
};

const DECORATOR_CHAINS: string[][] = [
    ['state', 'prop'],
    ['state', 'link'],
    ['state', 'observed', 'objectlink']
];

function normalizeDecorator(deco: unknown): string {
    if (typeof deco === 'object' && deco !== null && 'toString' in deco && typeof (deco as { toString: unknown }).toString === 'function') {
        return (deco as { toString: () => string }).toString()
            .replace(/^@/, '').replace(/Decorator$/, '').trim().toLowerCase();
    }
    return '';
}
function getFieldDecorators(field: ArkField): Set<string> {
    return new Set((field.getDecorators?.() || []).map(normalizeDecorator));
}

interface FileCheck {
    arkFile: ArkFile;
    issues: IssueReport[];
}

export class BadParameterPassingCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    public fileChecks: FileCheck[] = [];

    public registerMatchers(): MatcherCallback[] {
        return [{ matcher: undefined, callback: this.check }];
    }

    // === 检查入口 ===
    public check = (scene: Scene): void => {
        this.issues = [];
        this.defects = [];
        this.fileChecks = [];
        this.processFiles(scene);
        this.collectDefects();
    };

    /**
     * 拆分check方法的文件和类遍历逻辑
     */
    private processFiles(scene: Scene): void {
        for (const arkFile of scene.getFiles()) {
            this.processClasses(arkFile);
        }
    }

    private processClasses(arkFile: ArkFile): void {
        for (const clazz of arkFile.getClasses()) {
            this.processClassWithParamDepthMap(clazz);
        }
    }

    private processClassWithParamDepthMap(clazz: ArkClass): void {
        const viewTreeRoot = clazz.getViewTree()?.getRoot();
        if (!viewTreeRoot) {
            return;
        }
        for (const field of clazz.getFields()) {
            this.processFieldForParamDepth(clazz, viewTreeRoot, field);
        }
    }

    private processFieldForParamDepth(
        clazz: ArkClass,
        viewTreeRoot: ViewTreeNode,
        field: ArkField
    ): void {
        if (!this.isArkField(field)) {
            return;
        }
        const decos = getFieldDecorators(field);
        if (!decos.has('state')) {
            return;
        }
        for (const chain of DECORATOR_CHAINS) {
            this.processChainForField(clazz, viewTreeRoot, field, chain, decos);
        }
    }

    private processChainForField(
        clazz: ArkClass,
        viewTreeRoot: ViewTreeNode,
        field: ArkField,
        chain: string[],
        decos: Set<string>
    ): void {
        if (chain[0] !== 'state') {
            return;
        }
        let matchedChainIndex = 0;
        while (chain[matchedChainIndex] && decos.has(chain[matchedChainIndex])) {
            matchedChainIndex++;
        }
        if (matchedChainIndex > 0) {
            const paramDepthMap = new Map<string, number>();
            this.traverseParamChainByName(viewTreeRoot, field.getName(), 1, paramDepthMap, chain, 1);
            for (const [key, maxDepth] of paramDepthMap.entries()) {
                this.handleParamDepthEntry(clazz, key, chain, maxDepth, field);
            }
        }
    }

    private handleParamDepthEntry(
        clazz: ArkClass,
        key: string,
        chain: string[],
        maxDepth: number,
        field: ArkField
    ): void {
        const [paramName, chainIdxStr] = key.split('::');
        const chainIdx = parseInt(chainIdxStr, 10);
        if (chainIdx === chain.length - 1 && maxDepth >= 3) {
            this.reportIssue(clazz, paramName, chain, maxDepth, field);
        }
    }

    /**
     * 递归遍历组件树，追踪参数名传递深度，支持链路装饰器
     */
    private traverseParamChainByName(
        node: ViewTreeNode,
        paramName: string,
        depth: number,
        paramDepthMap: Map<string, number>,
        chain: string[],
        chainIdx: number
    ): void {
        const isLinkChain = chain.includes('link');
        if (chainIdx === chain.length - 1 || (isLinkChain && chainIdx >= chain.length - 1)) {
            const key = `${paramName}::${chain.length - 1}`;
            paramDepthMap.set(key, Math.max(paramDepthMap.get(key) || 0, depth));
        }
        if (!node.children || node.children.length === 0) {
            return;
        }
        this.processParamChainChildren(node.children, paramName, depth, paramDepthMap, chain, chainIdx, isLinkChain);
    }

    private processParamChainChildren(
        children: ViewTreeNode[],
        paramName: string,
        depth: number,
        paramDepthMap: Map<string, number>,
        chain: string[],
        chainIdx: number,
        isLinkChain: boolean
    ): void {
        for (const child of children) {
            this.traverseParamChainByNameChild(child, paramName, depth, paramDepthMap, chain, chainIdx, isLinkChain);
        }
    }

    private traverseParamChainByNameChild(
        child: ViewTreeNode,
        paramName: string,
        depth: number,
        paramDepthMap: Map<string, number>,
        chain: string[],
        chainIdx: number,
        isLinkChain: boolean
    ): void {
        let found = false;
        let nextChainIdx = chainIdx;
        if (child.stateValues && child.stateValues.size > 0) {
            found = this.checkChildStateValues(child, paramName, chain, chainIdx, isLinkChain);
            if (found && isLinkChain) {
                nextChainIdx = Math.min(chainIdx + 1, chain.length - 1);
            } else if (found) {
                nextChainIdx = chainIdx + 1;
            }
        }
        if (found) {
            this.traverseParamChainByName(child, paramName, depth + 1, paramDepthMap, chain, nextChainIdx);
        } else {
            this.traverseParamChainByName(child, paramName, depth, paramDepthMap, chain, chainIdx);
        }
    }

    private checkChildStateValues(
        child: ViewTreeNode,
        paramName: string,
        chain: string[],
        chainIdx: number,
        isLinkChain: boolean
    ): boolean {
        for (const field of child.stateValues) {
            if (field.getName && field.getName() === paramName) {
                if (isLinkChain) {
                    return true;
                } else if (getFieldDecorators(field).has(chain[chainIdx])) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 收集 defects 并按文件归类
     */
    private collectDefects(): void {
        this.defects = this.issues.map(issue => issue.defect);
        this.fileChecks = [];
        for (const defect of this.defects) {
            this.processDefectForFileChecks(defect);
        }
    }

    private processDefectForFileChecks(defect: Defects): void {
        // 通过mergeKey解析filePath，mergeKey格式：filePath%fixKey%description
        const filePath = defect.mergeKey.split('%')[0];
        let fileCheck = this.fileChecks.find(f => String(f.arkFile.getFilePath()) === String(filePath));
        if (!fileCheck) {
            fileCheck = { arkFile: { getFilePath: () => filePath } as ArkFile, issues: [] };
            this.fileChecks.push(fileCheck);
        }
        fileCheck.issues.push({ defect } as IssueReport);
    }

    private getFilePath(arkClass: ArkClass, field?: ArkField): string {
        const fieldClass = field?.getDeclaringArkClass?.();
        const filePathRaw = fieldClass?.getDeclaringArkFile?.().getFilePath?.() ?? arkClass.getDeclaringArkFile().getFilePath() ?? '';
        return path.normalize(filePathRaw);
    }

    private getLine(arkClass: ArkClass, field?: ArkField): number {
        const fieldClass = field?.getDeclaringArkClass?.();
        const pos = typeof field?.getOriginPosition === 'function' ? field.getOriginPosition() : undefined;
        return pos?.getLineNo?.() ?? fieldClass?.getLine?.() ?? arkClass.getLine();
    }

    private getCol(arkClass: ArkClass, field?: ArkField): number {
        const fieldClass = field?.getDeclaringArkClass?.();
        const pos = typeof field?.getOriginPosition === 'function' ? field.getOriginPosition() : undefined;
        return pos?.getColNo?.() ?? fieldClass?.getColumn?.() ?? arkClass.getColumn();
    }

    private getEndCol(arkClass: ArkClass, col: number, field?: ArkField): number {
        if (typeof col === 'number' && field?.getName?.()) {
            return col + field.getName().length - 1;
        }
        return col ?? 1;
    }

    /**
     * 生成告警 defect
     */
    private reportIssue(
        arkClass: ArkClass,
        propName: string,
        chain: string[],
        depth: number,
        field?: ArkField
    ): void {
        const filePath = this.getFilePath(arkClass, field);
        const line = this.getLine(arkClass, field);
        const col = this.getCol(arkClass, field);
        const endCol = this.getEndCol(arkClass, col, field);
        const defect = new Defects(
            line,
            col,
            endCol,
            this.metaData.description,
            this.rule?.alert ?? this.metaData.severity,
            this.rule?.ruleId ?? '',
            filePath,
            this.metaData.ruleDocPath,
            true,
            false,
            false
        );
        this.issues.push(new IssueReport(defect, undefined));
    }

    private isArkField(field: unknown): field is ArkField {
        return !!field && typeof (field as ArkField).getDecorators === 'function';
    }
}
